package biz.datalk.industrialland.admin.modules.common.servie.impl;

import biz.datalk.commons.utils.IdProvider;
import biz.datalk.commons.utils.ip.IPUtil;
import biz.datalk.industrialland.admin.component.shiro.CustomLoginUser;
import biz.datalk.industrialland.admin.config.properties.IndustrialLandCoreProperties;
import biz.datalk.industrialland.admin.modules.common.pojo.dto.CaptchaDTO;
import biz.datalk.industrialland.admin.modules.common.pojo.dto.CodeDTO;
import biz.datalk.industrialland.admin.modules.common.pojo.dto.LoginWithCodeDTO;
import biz.datalk.industrialland.admin.modules.common.pojo.vo.CaptchaVO;
import biz.datalk.industrialland.admin.modules.common.pojo.vo.GetUserInfoVO;
import biz.datalk.industrialland.admin.modules.common.pojo.vo.LoginWithCodeVO;
import biz.datalk.industrialland.admin.modules.common.servie.IAuthService;
import biz.datalk.industrialland.admin.modules.common.servie.ICaptchaService;
import biz.datalk.industrialland.admin.modules.externalapi.model.vo.SmsResult;
import biz.datalk.industrialland.admin.modules.externalapi.service.ISmsService;
import biz.datalk.industrialland.admin.modules.util.ShiroUtil;
import biz.datalk.industrialland.common.config.JwtCryptoCfg;
import biz.datalk.industrialland.common.def.CommonConstant;
import biz.datalk.industrialland.common.def.enums.DBYNEnum;
import biz.datalk.industrialland.common.exception.ServiceException;
import biz.datalk.industrialland.common.result.UnifyResultCode;
import biz.datalk.industrialland.common.util.DesensitizeUtil;
import biz.datalk.industrialland.common.util.JwtUtil;
import biz.datalk.industrialland.common.util.RedisUtil;
import biz.datalk.industrialland.common.util.SpringContextUtils;
import biz.datalk.industrialland.mbg.mapper.SysUserMapper;
import biz.datalk.industrialland.mbg.pojo.po.SysUser;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;

/**
 * @author tarofang@163.com
 * @date 2023-09-05
 */
@Slf4j
@Service
public class AuthServiceImpl implements IAuthService {

    private final ICaptchaService captchaService;
    private final RedisUtil redisUtil;
    private final IndustrialLandCoreProperties industrialLandCoreProperties;
    private final ISmsService smsService;
    private final SysUserMapper sysUserMapper;

    @Autowired
    public AuthServiceImpl(
            ICaptchaService captchaService, //
            RedisUtil redisUtil,  //
            IndustrialLandCoreProperties industrialLandCoreProperties, //
            ISmsService smsService, //
            SysUserMapper sysUserMapper) {
        this.captchaService = captchaService;
        this.redisUtil = redisUtil;
        this.industrialLandCoreProperties = industrialLandCoreProperties;
        this.smsService = smsService;
        this.sysUserMapper = sysUserMapper;
    }

    @Override
    public CaptchaVO captcha(CaptchaDTO dto) {
        return captchaService.create(dto);
    }


    @Override
    public void code(CodeDTO dto) {
        // 校验图形验证码
        this.checkImgCaptcha(dto.getRan(), dto.getCaptcha(), Boolean.FALSE);
        String mobile = dto.getMobile();

        // 同一个IP 每分钟请求 20 次
        HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
        String remoteIp = IPUtil.getIpAddress(request);
        String ymdhm = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));
        String limitCacheKey = String.format(CommonConstant.REQ_SMS_CODE_LIMIT_CACHE_KEY, remoteIp, ymdhm);
        long incr = redisUtil.incr(limitCacheKey, 1);
        if (incr == 1) {
            redisUtil.expire(limitCacheKey, 60);
        }
        if (incr > industrialLandCoreProperties.getApiAuthCodeSameIpPreMinuteLimit()) {
            log.warn("Send code over frequency. mobile {}, ip {}, times {}", mobile, remoteIp, incr);
            throw new ServiceException("请求验证码太频繁");
        }

        // 判定是否已发送
        String dynCodeCacheKey = String.format(CommonConstant.REQ_SMS_CODE_CACHE_KEY, mobile);
        String cacheDynCode = redisUtil.getStr(dynCodeCacheKey);
        if (StringUtils.isNotBlank(cacheDynCode)) {
            log.debug("The code has been sent. mobile {}, ip {}, cacheDynCode {}", mobile, remoteIp, cacheDynCode);
            throw new ServiceException("验证码已发送");
        }

        // 发送验证码
        String smsCode = IdProvider.createDynmaicCode(industrialLandCoreProperties.getApiAuthCodeCodeLength(), Boolean.TRUE);

        SmsResult smsResult = smsService.sendCode(mobile, smsCode);
        Assert.isTrue(smsResult.isSuccess(), () -> {throw new ServiceException("短信验证码发送失败");});

        redisUtil.set(dynCodeCacheKey, smsCode, industrialLandCoreProperties.getApiAuthCodeCodeTimeoutSeconds());
    }

    /**
     * 校验图形验证码
     *
     * @param ran     图形验证码标识
     * @param captcha 图形验证码
     * @param delete  是否删除图形验证码
     *
     * @author tarofang@163.com
     * @date 2024年01月15日
     */
    private void checkImgCaptcha(String ran, String captcha, boolean delete) {
        Boolean loginUseImgCaptcha = industrialLandCoreProperties.getLoginUseImgCaptcha();
        if (loginUseImgCaptcha != null && loginUseImgCaptcha) {
            if (!captchaService.checkRandCode(captcha, ran, delete)) {
                throw new ServiceException("图形验证码错误");
            }
        }
    }

    @Override
    public void loginOut() {
        CustomLoginUser customLoginUser = ShiroUtil.currentLoginUser();
        String accessToken = JwtUtil.getRequestToken();
        if (StringUtils.isBlank(accessToken)) {
            throw new ServiceException(UnifyResultCode.TOKEN_TIMEOUT, "您未登录或长时间未操作");
        }
        String tokenRedisKey = CommonConstant.PREFIX_USER_TOKEN.concat(accessToken);
        String latestTokenRedisKey = CommonConstant.PREFIX_USER_TOKEN_LATEST + customLoginUser.getUserNo();
        redisUtil.del(tokenRedisKey, latestTokenRedisKey);
    }

    @Override
    public SysUser selectByUsername(String username) {
        if (StringUtils.isBlank(username)) {
            return null;
        }
        return sysUserMapper.selectByUsername(username.trim());
    }

    @Override
    public LoginWithCodeVO loginWithCode(LoginWithCodeDTO dto) {
        // 校验图形验证码
        this.checkImgCaptcha(dto.getRan(), dto.getCaptcha(), Boolean.TRUE);

        // 校验手机验证码
        String mobile = dto.getMobile();
        String dynCodeCacheKey = String.format(CommonConstant.REQ_SMS_CODE_CACHE_KEY, mobile);
        String cacheDynCode = redisUtil.getStr(dynCodeCacheKey);
        String supportCode = industrialLandCoreProperties.getApiAuthLoginSupportCode();
        if (StringUtils.isBlank(supportCode) && StringUtils.isBlank(cacheDynCode)) {
            throw new ServiceException("验证码已失效");
        }
        if (!StringUtils.equals(cacheDynCode, dto.getCode()) && !StringUtils.equals(supportCode, dto.getCode())) {
            log.warn("Invalid code. [mobile={}, code={}, cacheCode={}, supportCode={}]", mobile, dto.getCode(), cacheDynCode, supportCode);
            throw new ServiceException("验证码错误");
        }

        // 查询手机号是否存在 ==> 不存在则直接提示: 用户不存在或已被删除
        List<SysUser> sysUsers = sysUserMapper.selectByPhone(mobile, 1, DBYNEnum.N.name());
        if (CollectionUtils.isEmpty(sysUsers)) {
            log.warn("The user no exists. [mobile={}]", mobile);
            throw new ServiceException("用户不存在或已被删除");
        }
        if (CollectionUtils.size(sysUsers) != 1) {
            log.warn("There are multiple users with the same mobile phone number. [mobile={}]", mobile);
            throw new ServiceException("用户不存在或已被删除");
        }

        SysUser sysUser = sysUsers.get(0);

        // 签发 token
        String jwtToken = this.genJwtToken(sysUser.getId(), sysUser.getUsername(), mobile, CommonConstant.APP_KEY_LAND);

        // 手机号脱密
        String mobileDsn = DesensitizeUtil.desensitizeMobile(mobile);

        LoginWithCodeVO vo = new LoginWithCodeVO();
        vo.setId(sysUser.getId());
        vo.setUserNo(sysUser.getUsername());
        vo.setMobile(mobile);
        vo.setMobDsn(mobileDsn);
        vo.setToken(jwtToken);
        return vo;
    }

    @Override
    public GetUserInfoVO getUserInfo() {
        CustomLoginUser customLoginUser = ShiroUtil.currentLoginUser();
        // SysUser sysUser = customLoginUser.getSysUser();

        GetUserInfoVO vo = new GetUserInfoVO();
        BeanUtils.copyProperties(customLoginUser, vo);
        return vo;
    }


    private String genJwtToken(long sysUid, String userNo, String username, String appKey) {
        Map<String, String> payloads = Maps.newHashMap();
        payloads.put(JwtUtil.PAYLOAD_SYS_USER_ID, String.valueOf(sysUid));
        payloads.put(JwtUtil.PAYLOAD_USER_NO, StringUtils.trimToEmpty(userNo));
        payloads.put(JwtUtil.PAYLOAD_USERNAME, StringUtils.trimToEmpty(username));
        payloads.put(JwtUtil.PAYLOAD_APPKEY, StringUtils.trimToEmpty(appKey));

        String jwtToken = JwtUtil.sign(payloads);
        String tokenRedisKey = CommonConstant.PREFIX_USER_TOKEN.concat(jwtToken);
        long tokenExpire = JwtCryptoCfg.getInstance().getExpire();
        redisUtil.set(tokenRedisKey, jwtToken, tokenExpire);
        // 最后一次登录的 token ==> 用于限定 只能在单一地点登录
        String latestTokenRedisKey = CommonConstant.PREFIX_USER_TOKEN_LATEST + userNo;
        redisUtil.set(latestTokenRedisKey, jwtToken, tokenExpire);
        return jwtToken;
    }
    // private String genJwtToken(String userNo, String appKey) {
    //     String jwtToken = JwtUtil.sign(userNo, appKey);
    //     String tokenRedisKey = CommonConstant.PREFIX_USER_TOKEN.concat(jwtToken);
    //     long tokenExpire = JwtCryptoCfg.getInstance().getExpire();
    //     redisUtil.set(tokenRedisKey, jwtToken, tokenExpire);
    //     // 最后一次登录的 token ==> 用于限定 只能在单一地点登录
    //     String latestTokenRedisKey = CommonConstant.PREFIX_USER_TOKEN_LATEST + userNo;
    //     redisUtil.set(latestTokenRedisKey, jwtToken, tokenExpire);
    //     return jwtToken;
    // }


}
